Storage and Retrieval
Bitcask
-
與Dictionary很相似,通常都是用hash table實現的
-
假設我們的數據存儲只是一個加寫入的文件,使用in-memory hash map,每個key都map到一個數據文件的byte offset,指明可以找到對應值的位置。
-
提供了高性能的讀取和寫入,但所有key必須放入可用的memory中
-
為了最終避免用完disk space,將日志分為特定大小的段,當日志增長到特定size時關閉當前段文件,并開始寫入一個新的segment文件,然後對這些segment進行compaction,即丟棄重復的key,只保留每個key的最近更新。
-
由於compaction會使得segment變得很小(如果一個key被重寫了很多次),我們可以將多個segment合并在一起。segment被寫入後永遠不會被修改,會被寫入一個新的文件。
-
凍結段的compaction和merging可以在後台thread中完成,在進行時,我們仍然可以繼續使用舊的段文件來正常提供讀寫請求。合并完成後,我們將讀取請求轉換為使用新的合并段而不是舊段,然後可以簡單地刪除舊的段文件。
-
每個段都有自己的in-memory hash table,將keys map到文件的offset。在找key的值,我們會先找最新的段的hash map,如果不存在,我們會找第二個最新的段,如此類推。
-
有以下細節需要留意:
- File Format: CSV不是日志的最佳格式,使用binary format更快。首先以byte為單位對string的長度進行encoding,然後使用raw string(不需要escaping轉義)
- Deleting Records: 如果要刪除一個key及其value,則必須在數據文件(有時稱為tombstone)中附加一個特殊的刪除紀錄。當日志段被合并時,tombstone會告訴合并過程去discard刪除鍵的任何以前的value。
- Crash Recovery: 如果db重啟,in-memory hash map將丟失。理論上,可以通過從頭到尾讀取整個段文件,并根據每個鍵的最近值的offset來恢復每個段的hash map,但這將要很長時間,如果文件是超大。Bitcask通過恢復disk上每個segment的hash map的snapshot,可以更快地加載到內存中。
- Partially Written Records: db可能隨時崩潰,可以halfway through appending record to log。Bitcask文件包含checksum來檢測和忽略這些損壞部份。
- Concurrency Control: write操作是以嚴格的順序加到日志中,最常見的實現是只有一個writer thread,segments是append-only和immutable,所以它們可以被多個threads同時讀取。
- 但為什麼update file而要用append-only的方式?
- appending和segment merging是順序的寫入操作,比random writes快很多,特別在magnetic spinning disk hard drive。
- 會令concurrency和crash recovery變得簡單,例如不必擔心value被overwritten時發生crash,留下一個同時有新和舊值的文件。
- merging可以避免數據文件隨時間推移而變得快散。
- appending和segment merging是順序的寫入操作,比random writes快很多,特別在magnetic spinning disk hard drive。
- 但為什麼update file而要用append-only的方式?
- hash table的局限性: hash table只能放進memory,如果有太多keys會產生問題。理論上可以把hash map放到disk,但不幸地會影響性能。它需要大量的random access I/O,當它變滿時是很昂貴的,而且要解決hash collision。
- range queries的效率不高,例如無法輕鬆scan "kitty00000"和"kitty99999"